這是Reactv18開始有的問題,官方描述:
While rendering your application, there was a difference between the React tree that was pre-rendered (SSR/SSG) and the React tree that rendered during the first render in the Browser. The first render is called Hydration which is a feature of React。
我的理解是:
因為Nextjs啟動時會同時產生Server Side與Client Side (兩個都包含React tree),Server端React tree 是在 pre-rendered (SSR/SSG) 產生的,而Client端React Tree是first render in the Browser (又叫做Hydration),當兩者認知的React Tree不一樣時就會發生此錯誤!!!
(不一樣會導致 React Tree與 DOM 不同步,並導致出現意外的內容/屬性。)
這個問題對應的情境其實有好幾個,我先列我遇到的:
Whenever you are creating a component that has dynamic data. Try importing that component in below mentioned format. It disables SSR and lets you render new data on API call.
(意思是如果該Component需要動態撈資料,則透過dymanic關閉SSR渲染達到動態目的,避免Server端一開始先渲染導致與Client不一樣而衝突)
//===============React18版本會出錯==============//
import ProductCard from "./Product"; //這個Component會動態去載入圖片
//================可以運作的方法=================//
// Whenever you are creating a component that has dynamic data. Try importing that component in below mentioned format.
// It disables SSR and lets you render new data on API call.
import dynamic from 'next/dynamic';
const ProductCard = dynamic(() => import("./Product"), {
ssr: false,
});
const ProductHome = () => {
const router = useRouter();
const { id } = router.query;
if (!id) return <></>; //防止因為第一次渲染沒拿到id而出問題
const product = getProductById(id as string);
return (
<div>
...
<ProductCard product={product} all />
</div>
);
};
另外,我也想到另外可以使用Nextjs提供的SSG功能,讓需要動態撈取資料的部份先放到getStaticProps中,讓Server端在第一次渲染之前就先獲取資料,然後再建置出完整的html檔案。
import {GetStaticProps} from 'next';
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
export const getStaticProps:GetStaticProps = async ()=>{
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const post: Post = await res.json();
return {
props: {
post,
},
}
}
這個方法我沒有實際試過但我認為應該可行!
以下是官方列的問題情境:
下方是個簡單範例告訴你當Client端與Server端分開渲染會發生什麼事
function MyComponent() {
// This condition depends on `window`.
// During the first render of the browser the `color` variable will be different
const color = typeof window !== 'undefined' ? 'red' : 'blue'
// As color is passed as a prop there is a mismatch
// between what was rendered server-side vs what was rendered in the first render
return <h1 className={`title ${color}`}>Hello World!</h1>
}
上方color在server端會是”blue”,但在Client端 (Browser)會是 “red”!!!
解決方法是使用useState()與useEffect(),依據官方描述,因為useEffect()只會在Client端運作,所以可以確保先後順序是Client觸發了,Client與Server再一起改變!! (而不是兩者先各做各的)
// In order to prevent the first render from being different
// you can use `**useEffect`** which is only executed in the browser
// and is executed during hydration
import { useEffect, useState } from 'react'
function MyComponent() {
// The default value is 'blue',
// it will be used during pre-rendering
// and the first render in the browser (hydration)
const [color, setColor] = useState('blue')
// During hydration `useEffect` is called. `window` is available in `useEffect`.
// In this case because we know we're in the browser checking for window is not needed.
// If you need to read something from window that is fine.
// By calling `setColor` in `useEffect` a render is triggered after hydrating,
// this causes the "browser specific" value to be available. In this case 'red'.
useEffect(() => {
setColor('red')
}, [])
// As color is a state passed as a prop there is no mismatch
// between what was rendered server-side vs what was rendered in the first render.
// After useEffect runs the color is set to 'red'
return <h1 className={`title ${color}`}>Hello World!</h1>
}
上面範例的意思是,因為使用useState,所以Server與Client一開始都會看到color是”Blue”!!!
當前後端渲染完成後,由Client觸發useEffect()做componentDidMount()觸發setColor(),改變color的同時前後端也會同步,因此兩者對於color的認知都會變成color=”red”!!!
Invalid HTML may cause hydration mismatch (such as div inside p)
這個意思是說,如果在HTML結構上使用法不正確,可能會導致Client與Server看到的結果不一樣。
我的認知是,因為Client是在Browser上運作,受限於Browser規範導致錯誤的HTML在Client會以不一樣的姿態渲染,導致兩者不同。
官方範例:
return (
<p>
<div>
This is not correct and should never be done because the p tag has been
abused
</div>
<Image src="/vercel.svg" alt="" width="30" height="30" />
</p>
)
}
修復方式就是用正確的邏輯撰寫:
export const CorrectComponent = () => {
return (
<div>
<div>
This is correct and should work because a div is really good for this
task.
</div>
<Image src="/vercel.svg" alt="" width="30" height="30" />
</div>
)
官方列出可能發生在css-in-js類型的libraries:
參考資料:
https://github.com/vercel/next.js/discussions/35773#discussioncomment-2840696
https://nextjs.org/docs/messages/react-hydration-error